<?php

/**
 * Albireo Kernel
 *
 * @copyright  Copyright (c) 2010 Albireo Solutions
 * @package    Kernel
 */
namespace KernelModule\AclModule;

use \Nette\Object;
use \Nette\Collections\ArrayList;

use \dibi;
use \DibiDataSource;

use \InvalidArgumentException;
use \InvalidStateException;

use Celebrio\AccessControl\UserManagement;

/**
 * Acl Management
 *
 * @author Albireo Solutions
 */

class AclManagement extends Object {

    private $roles = "roles";

    private $permissions;
    private $privileges;
    private $resources;

    public function __construct($type = "basic") {
        if ($type == "basic") {
            $this->permissions = "acl";
            $this->privileges = "privileges";
            $this->resources = "resources";
        } else if ($type == "fs") {
            $this->permissions = "fpermissions";
            $this->privileges = "fprivileges";
            $this->resources = "resources";
        } else throw new InvalidArgumentException ("invalid ACL type");
    }
    
    /**
     * Adds Role to Database
     *
     * @param AclRole $role
     * @return bool Sets whether role is in DB or was inserted
     * 
     * @throws DibiException when database problem occurs
     */
    public function addRole(AclRole $role) {
        if (is_null($role->getName())) {
            return false;
        }
        
        $res = dibi::fetch("
                SELECT * FROM [$this->roles]
                WHERE [name]=%s", $role->getName());
        if ($res) {
            $role->setId((int)$res["id"]);
            $role->setParentId((int)$res["parent_id"]);
            $role->setCreated($res["created"]);
            return true;
        } else {
            dibi::query("
                    INSERT INTO [$this->roles]([name],[parent_id])
                    VALUES (%sN,%iN)", $role->getName(), $role->getParentId());
            $role->setId(dibi::getInsertId());
            return true;
        }
    }

    /**
     * Adds resource to Database
     *
     * @param AclResource $resource
     * @return bool Sets whether resource is in DB or was inserted
     * 
     * @throws DibiException when database problem occurs
     */
    public function addResource(AclResource $resource) {
        if (is_null($resource->getName())) {
            return false;
        }
        $res = dibi::fetch("
                SELECT * FROM [$this->resources] 
                WHERE [name]=%s", $resource->getName());
        if ($res) {
            $resource->setId((int)$res["id"]);
            $resource->setCreated($res["created"]);
            return true;
        } else {
            dibi::query("
                    INSERT INTO [$this->resources]([name])
                    VALUES (%sN)", $resource->getName());
            $resource->setId(dibi::getInsertId());
            return true;
        }
    }

    /**
     * Adds privilege to Database
     *
     * @param AclPrivilege $privilege
     * @return bool Sets whether resource is in DB or was inserted
     * 
     * @throws DibiException when database problem occurs
     */
    public function addPrivilege(AclPrivilege $privilege) {
        if (is_null($privilege->getName())) {
            return false;
        }
        
        $res = dibi::fetch("
                SELECT * FROM [$this->privileges]
                WHERE [name]=%s", $privilege->getName());
        if ($res) {
            $privilege->setId((int)$res["id"]);
            $privilege->setCreated($res["created"]);
            return true;
        } else {
            dibi::query("
                    INSERT INTO [$this->privileges]([name])
                    VALUES (%sN)", $privilege->getName());
            $privilege->setId(dibi::getInsertId());
            return true;
        }
    }

    /**
     * Adds ACL to Database
     *
     * @param Acl $acl
     * @return bool Sets whether ACL is in DB or was inserted
     * 
     * @throws DibiException when database problem occurs
     */
    public function addAcl(Acl $acl) {
        if (!$this->isAclValid($acl)) return false;
        $alreadyExists = $this->getDataSource("acl")->
                where("[privilege_id] = ".$acl->getPrivilege()->getId())->
                where("[resource_id] = ".$acl->getResource()->getId())->
                where("[role_id] = ".$acl->getRole()->getId())->
                getResult()->fetchAll();
        switch(count($alreadyExists)) {
            case 0 :
                dibi::query("INSERT INTO [" . $this->permissions . "]([role_id], [privilege_id], [resource_id], [allowed]) VALUES
                        (%i, %i, %i, %b);", $acl->getRole()->getId(), $acl->getPrivilege()->getId(),
                        $acl->getResource()->getId(), $acl->getAllowed());
                        $acl->setId(dibi::getInsertId());
                return true;
            case 1 :
                $row = $alreadyExists[0];
                dibi::query("UPDATE [" . $this->permissions . "] SET [allowed]=%b WHERE [id]=%i", $acl->getAllowed(), $row["id"]);
                return false;
            default : //this should never happen
                throw new InvalidStateException("This-should-never-happen error in addAcl");
        }

//            dibi::query("INSERT INTO [" . $this->permissions . "]([role_id],[privilege_id],[resource_id],[allowed]) VALUES
//                        (%iN,%iN,%iN,%b)", $acl->getRole()->getId(), $acl->getPrivilege()->getId(),
//                    $acl->getResource()->getId(), $acl->getAllowed());
//            $acl->setId(dibi::getInsertId());
//            return true;

    }

    private function isAclValid($acl) {
        if (is_null($acl->getRole()) || is_null($acl->getResource()) || is_null($acl->getPrivilege())) {
            return false;
        }

        if (($acl->getRole()->getId() == 0) ||
                ($acl->getResource()->getId() == 0) ||
                ($acl->getPrivilege()->getId() == 0)) {
            return false;
        }
        return true;
    }

    /**
     * This method updates the ACL row identified by the ID of the ACL. All attributes
     * can be changed by this method. If another row with the same privilege, resource and
     * role exists, false is returned.
     *
     * @param Acl $acl
     * 
     * @throws DibiException when database problem occurs
     */
    public function updateAcl($acl) {
        if (!$this->isAclValid($acl)) throw new InvalidArgumentException("incorrect ACL given");
        $alreadyExists = $this->getDataSource("acl")->
                where("[privilege_id] = ".$acl->getPrivilege()->getId())->
                where("[resource_id] = ".$acl->getResource()->getId())->
                where("[role_id] = ".$acl->getRole()->getId())->
                getResult()->fetchAll();
        if (count($alreadyExists) > 0 && $alreadyExists[0]["id"] != $acl->getId()) {
            return false;
        }
        dibi::query("UPDATE [" . $this->permissions . "] SET [role_id]=%i, [privilege_id]=%i, [resource_id]=%i, [allowed]=%b WHERE [id]=%i;",
                $acl->getRole()->getId(),
                $acl->getPrivilege()->getId(),
                $acl->getResource()->getId(),
                $acl->getAllowed(),
                $acl->getId());
        return true;
    }

    /**
     * Adds one acl role to database (if role, resource or privilege doesn't exist it tries to insert it)
     * if $userName is defined then it connects role with the user
     *
     * @param string $roleName
     * @param string $resourceName
     * @param string $privilegeName
     * @param bool $allowed
     * @param string $userName
     */
    public function addOneRule($roleName, $resourceName, $privilegeName, $allowed, $userName = null) {
        $role = new AclRole();
        $role->setName($roleName);
        $this->addRole($role);

        if (!empty($userName)) {
            $userManagement = new UserManagement();
            $user = $userManagement->getUserByUsername($userName);
            $userManagement->addUserToRole($user,$role);
        }

        $resource = new AclResource();
        $resource->setName($resourceName);
        $this->addResource($resource);

        $privilege = new AclPrivilege();
        $privilege->setName($privilegeName);
        $this->addPrivilege($privilege);

        $acl = $this->getAclFromAttributes($role, $resource, $privilege, $allowed);
        $this->addAcl($acl);
    }

    /**
     * Returns ACL from given IDs
     *
     * @param int $roleId
     * @param int $resourceId
     * @param int $privilegeId
     * @param bool $allowed
     */
    public function getAclFromIds($roleId, $resourceId, $privilegeId, $allowed = NULL) {
        $role = new AclRole();
        $role->setId($roleId);
        //$this->addRole($role);

        $resource = new AclResource();
        $resource->setId($resourceId);
        //$this->addResource($resource);

        $privilege = new AclPrivilege();
        $privilege->setId($privilegeId);
        //$this->addPrivilege($privilege);

        $acl = $this->getAclFromAttributes($role, $resource, $privilege, $allowed);
        return $acl;
        //$this->addAcl($acl);
    }

    //TODO IMPLEMENTOVAT
    public function getAclFromAttributes($role, $resource, $privilege, $allowed = false) {
        $acl = new Acl();
        $acl->setRole($role);
        $acl->setResource($resource);
        $acl->setPrivilege($privilege);
        $acl->setAllowed($allowed);
        return $acl;
    }

    /**
     * @param String tablename of the data we want to get (e.g "privileges" or "resources")
     * @return DibiDataSource data source from table acording to the parameter
     */
    public function getDataSource($table) {
        return new DibiDataSource($table, dibi::getConnection());
    }

    /**
     * Gets the datasource with all "names", allowed and id of ACL table.
     *
     * @return DibiDataSource
     */
    public function getPermissionsDataSource() {
        $query = "
            SELECT 
                a.id AS id, 
                rol.name AS role, 
                res.name AS resource, 
                p.name AS privilege, 
                a.allowed AS allowed
            FROM 
                $this->permissions AS a LEFT JOIN
                $this->roles AS rol ON a.role_id = rol.id LEFT JOIN
                $this->resources AS res ON a.resource_id = res.id LEFT JOIN
                $this->privileges AS p ON a.privilege_id = p.id
            ORDER BY rol.name, res.name, p.name";
        $ds = $this->getDataSource($query);
        return $ds;
    }

    /**
     *
     * @param AclResource $resource
     * @return bool
     * 
     * @throws DibiException when database problem occurs
     */
    public function updateResource(AclResource $resource) {
        dibi::query("
                UPDATE [$this->resources] SET %a WHERE id = %i",
                array('name' => $resource->getName()), $resource->getId());
        return true;
    }

    private function tablenameOfItem($item) {
        if ($item instanceof AclResource) return $this->resources;
        else if ($item instanceof AclPrivilege) return $this->privileges;
        else if ($item instanceof AclRole) return $this->roles;
        else if ($item instanceof Acl) return $this->permissions;
        else throw new InvalidStateException ("Invalid ACL item type");
    }
    
    /**
     * Deletes the ACL item (Resource, Privilege, Role, Permission) given
     * as parameter from DB. Returns false when zero or negative ID
     * is given or when DB problem occurs.
     *
     * @param the item we want to delete (the only important
     *      thing which has to be set is its ID).
     * @return boolean TRUE when the item was successfuly deleted, FALSE otherwise
     * 
     * @throws DibiException when database problem occurs
     */
    public function deleteFromAcl($item) {
        if ($item->getId() <= 0) {
            return false;
        }
        
        $table = $this->tablenameOfItem($item);
        dibi::query("DELETE FROM [$table] WHERE [id] = %i", $item->getId());
        return true;
    }


    public function getPrivilegeById($id) {
        $privilegeArray = dibi::fetch("SELECT [id],[name],[created] FROM [" . $this->privileges . "] WHERE [id] = %i", $id);
        if ($privilegeArray) {
            $privilege = new AclPrivilege();
            $privilege->setId($privilegeArray["id"]);
            $privilege->setName($privilegeArray["name"]);
            $privilege->setCreated($privilegeArray["created"]);
            return $privilege;
        } else {
            return null;
        }
    }

    /**
     *
     * @param AclPrivilege $privilege
     * @return type 
     * 
     * @throws DibiException when database problem occurs
     */
    public function updatePrivilege(AclPrivilege $privilege) {
        dibi::query("
                UPDATE [$this->privileges] SET %a WHERE id = %i",
                array('name' => $privilege->getName()), $privilege->getId());
        return true;
    }

    /**
     *
     * @param type $id
     * @return AclResource 
     * 
     * @throws DibiException when database problem occurs
     */
    public function getResourceById($id) {
        $resourceArray = dibi::fetch("
                SELECT [id], [name], [created] 
                FROM [$this->resources] 
                WHERE [id] = %i", $id);
        if ($resourceArray) {
            $resource = new AclResource();
            $resource->setId($resourceArray["id"]);
            $resource->setName($resourceArray["name"]);
            $resource->setCreated($resourceArray["created"]);
            return $resource;
        } else {
            return null;
        }
    }
    

    public function getAccessForTheRole($roleId) {
        return dibi::fetchAll("
            SELECT r.name AS resource, p.name AS privilege, a.allowed AS allowed
            FROM 
                [$this->permissions] AS a INNER JOIN
                [$this->resources] AS r ON a.resource_id = r.id INNER JOIN
                [$this->privileges] AS p ON a.privilege_id = p.id
            WHERE [role_id] = %i
            ORDER BY r.name, p.name", $roleId);
    }

    /**
     *
     * @param type $id
     * @return Acl 
     * 
     * @throws DibiException when database problem occurs
     */
    public function getPermissionById($id) {
        $permRow = dibi::fetch("
                SELECT [id], [privilege_id], [resource_id], [role_id], [allowed]
                FROM [$this->permissions]
                WHERE [id]=%i", $id);
        if ($permRow) {
            $permission = new Acl();
            $resource = new AclResource();
            $role = new AclRole();
            $privilege = new AclPrivilege();

            $resource->setId($permRow["resource_id"]);
            $privilege->setId($permRow["privilege_id"]);
            $role->setId($permRow["role_id"]);
            //TODO implement more attributes when necessary

            $permission->setId($permRow["id"]);
            $permission->setResource($resource);
            $permission->setRole($role);
            $permission->setPrivilege($privilege);
            $permission->setAllowed($permRow["allowed"]);

            return $permission;
        } else {
            return null;
        }
    }

    public function getSearchDataSource($kind, $input, $limit = 0) {
        if ($kind == "permissions") {
            $ds = $this->getPermissionsDataSource();
        } else {
            $ds = $this->getDataSource($kind);
        }
        if ($kind == "resources") {
            $beginning = $input."%";
            $middle = "%:".$input."%";
            $ds->where("%or", array(
                array ("[name] LIKE %s", $beginning),
                array ("[name] LIKE %s", $middle),
            ));
        } else if ($kind == "privileges") {
            $ds->where("[name] LIKE %s", $input."%");
        } else if ($kind == "permissions") {
            $singleInput = $input."%";
            $middleInput = "%:".$input."%";
            $ds->where("%or", array(
                array ("[resource] LIKE %s", $singleInput),
                array ("[resource] LIKE %s", $middleInput),
                array ("[role] LIKE %s", $singleInput),
                array ("[privilege] LIKE %s", $singleInput),
            ));
        } else {
            throw new InvalidArgumentException("invalid \$kind parameter: ".$kind);
        }
        //$ds->orderBy("id");
        return $ds;
    }
}
